package org.compamatic;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.compamatic.annotations.CompaProperties;
import org.compamatic.annotations.CompaProperty;
import org.compamatic.annotations.CompaProperty.Order;

public abstract class Compamatic {
	@SuppressWarnings("rawtypes")
	private static final Map<String, Comparator> COMPARATOR_MAP = new ConcurrentHashMap<String, Comparator>();
	
	public static <T> int doComparasion(T t1, T t2) {
		Integer result = doCommonCompare(t1, t2);
		if (result != null) {
			return result;
		}
		
		Class propertyClazz = t1.getClass();
		Annotation annotation = getComparasionAnnotation(propertyClazz, null, "compareTo", propertyClazz);
		return comparasionImpl(t1, t2, annotation);
	}
	
	public static <T> int doComparasion(T t1, T t2,
			Class<? extends Comparator<T>> comparatorClazz) {
		Integer result = doCommonCompare(t1, t2);
		if (result != null) {
			return result;
		}

		Class propertyClazz = t1.getClass();
		Annotation annotation = getComparasionAnnotation(propertyClazz, comparatorClazz, "compare", propertyClazz, propertyClazz);
		return comparasionImpl(t1, t2, annotation);
	}
	
	private static Annotation getComparasionAnnotation(Class propertyClazz, Class comparatorClazz, String name, Class... methodClazz) {
		Class clazz = comparatorClazz != null ? comparatorClazz : propertyClazz;
		Annotation annotation = null;
		
		//Method annotation override class annotation
		try {
			Method method = getMethod(clazz, name, methodClazz);
			annotation = method.getAnnotation(CompaProperties.class);
			annotation = annotation != null ? annotation : method.getAnnotation(CompaProperty.class);
		} catch (SecurityException e) {
			throw new RuntimeException("SecurityException", e);
		}
		
		if (annotation != null) {
			return annotation;
		}
		
		return getComparasionAnnotation(clazz);
	}
	
	private static Annotation getComparasionAnnotation(Class clazz) {
		if (clazz == null) {
			return null;
		}
		
		Annotation annotation = clazz.getAnnotation(CompaProperties.class);
		annotation = annotation != null ? annotation : clazz.getAnnotation(CompaProperty.class);
		return annotation != null ? annotation : getComparasionAnnotation(clazz.getSuperclass());
	}
	
	private static <T> int comparasionImpl(T t1, T t2, Annotation annotation) {
		Integer result = doCommonCompare(t1, t2);
		if (result != null) {
			return result;
		}

		if (annotation instanceof CompaProperties) {
			CompaProperties compaProerties = (CompaProperties) annotation;
			CompaProperty[] properties = compaProerties.value();
			for (CompaProperty compaProerty : properties) {
				result = compareProperty(t1, t2, compaProerty);
				if (result != 0) {
					return result;
				}
			}
		} else if (annotation instanceof CompaProperty) {
			CompaProperty compaProerty = (CompaProperty) annotation;
			result = compareProperty(t1, t2, compaProerty);
		} else {
			throw new RuntimeException("Annotation not found!");
		}

		return result;
	}
	
	private static <T> int compareProperty(T t1, T t2, CompaProperty compaProerty) {

		Integer result = null;
		try {
			String property = compaProerty.property();
			Order sort = compaProerty.order();
			@SuppressWarnings("rawtypes")
			Class comparator = compaProerty.comparator();
			Field f1 = getField(t1.getClass(), property);
			if (!f1.isAccessible()) {
				f1.setAccessible(true);
			}

			Field f2 = getField(t2.getClass(), property);
			if (!f2.isAccessible()) {
				f2.setAccessible(true);
			}

			try {
				Object o1 = f1.isEnumConstant() ? f1 : f1.get(t1);
				Object o2 = f1.isEnumConstant() ? f1 : f2.get(t2);
				result = doCommonCompare(o1, o2);
				if (result != null) {
					return toggleResult(result, sort);
				}
				
				if (comparator != Void.class) {
					for (Class interfaze : comparator.getInterfaces()) {
						if (interfaze == Comparator.class) {
							try {
								Comparator comparatorInstance = COMPARATOR_MAP.get(comparator.getName());
								if (comparatorInstance == null) {
									comparatorInstance = (Comparator)comparator.newInstance();
									COMPARATOR_MAP.put(comparator.getName(), comparatorInstance);
								}
								result = toggleResult(comparatorInstance.compare(o1, o2), sort);
							} catch (InstantiationException e) {
								throw new RuntimeException("InstantiationException", e);

							}
						}
					}
					
				} else if (o1 instanceof Comparable && o1 instanceof Comparable) {
					return toggleResult(((Comparable)o1).compareTo((Comparable)o2), sort);
				} else {
					System.out.println("not a comparable instance!");
				}
			} catch (IllegalArgumentException e) {
				throw new RuntimeException("IllegalArgumentException", e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException("IllegalAccessException", e);
			}
		} catch (SecurityException e) {
			throw new RuntimeException("SecurityException", e);
		}

		return result;
	}
	
	private static Field getField(Class clazz, String name) {
		if (clazz == null) {
			return null;
		}
		
		try {
			// getDeclaredFields will return all fields declared in the current object
			// including private fields, but not fields in any parent objects.
			return clazz.getDeclaredField(name);
		} catch (SecurityException e) {
			throw new RuntimeException("NoSuchFieldException", e);
		} catch (NoSuchFieldException e) {
			// getFields returns all public fields visible on the object, including parents
			try {
				return clazz.getField(name);
			} catch (SecurityException e1) {
				throw new RuntimeException("SecurityException", e);
			} catch (NoSuchFieldException e1) {
				//recursive call
				return getField(clazz.getSuperclass(), name);
			}
		}
	}
	
	private static Method getMethod(Class clazz, String name, Class... methodClazz) {
		if (clazz == null) {
			return null;
		}
		
		try {
			return clazz.getDeclaredMethod(name, methodClazz);
		} catch (SecurityException e) {
			throw new RuntimeException("NoSuchMethodException", e);
		} catch (NoSuchMethodException e) {
			try {
				return clazz.getMethod(name, methodClazz);
			} catch (SecurityException e1) {
				throw new RuntimeException("SecurityException", e);
			} catch (NoSuchMethodException e1) {
				//recursive call
				if (methodClazz.length == 1) {
					return getMethod(clazz.getSuperclass(), name, methodClazz[0].getSuperclass());
				} else if (methodClazz.length == 2) {
					return getMethod(clazz.getSuperclass(), name, methodClazz[0].getSuperclass(), methodClazz[1].getSuperclass());
				} else {
					throw new IllegalArgumentException("Arguments length greater than two.");
				}
			}
		}
	}
	
	private static <E> Integer doCommonCompare(E e1, E e2) {
		// Default ascending order
		if (e1 == e2) {
			return 0;
		}

		if (e1 == null && e2 == null) {
			return 0;
		} else if (e1 == null && e2 != null) {
			return 1;
		} else if (e1 != null && e2 == null) {
			return -1;
		}
		
		return null;
	}
	
	private static int toggleResult(int result, CompaProperty.Order order) {
		switch (order) {
		case ASC:
			break;
		case DSC:
			result = -1 * result;
		}
		
		return result;
	}
}
